﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Compilation;
using System.Web.Mvc;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Core.Validation
{
    [ParseChildren(true)]
    [PersistChildren(false)]
    public class ModelValidator : WebControl
    {
        #region Members
        private readonly IControlValueResolver _controlValueResolver;
        private readonly ModelPropertyCollection _modelProperties = new ModelPropertyCollection();
        private Type _modelType;
        #endregion

        #region  Properties

        /// <summary>
        /// Gets the model properties.
        /// </summary>
        /// <value>The model properties.</value>
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public ModelPropertyCollection ModelProperties
        {
            get { return _modelProperties; }
        }

        /// <summary>
        /// Notifies the control that an element was parsed and adds the element to the <see cref="T:System.Web.UI.WebControls.Label"/> control.
        /// </summary>
        /// <param name="obj">An object that represents the parsed element.</param>
        protected override void AddParsedSubObject(object obj)
        {
            ModelProperty property = obj as ModelProperty;
            if (obj != null)
            {
                ModelProperties.Add(property);
            }
        }

        ///// <summary>
        ///// Returns true if the ModelType is specified.
        ///// </summary>
        ///// <returns></returns>
        //protected override bool ControlPropertiesValid()
        //{
        //    return !String.IsNullOrEmpty(ModelType);
        //}

        /// <summary>
        /// Gets or sets the type of the model.
        /// </summary>
        /// <value>The type of the model.</value>
        public string ModelType
        {
            get { return (string)ViewState["ModelType"]; }
            set { ViewState["ModelType"] = value; }
        }

        /// <summary>
        /// Gets or sets the validation summary ID.
        /// </summary>
        /// <value>The validation summary ID.</value>
        public string ValidationSummaryID
        {
            get { return (string)ViewState["ValidationSummaryID"]; }
            set { ViewState["ValidationSummaryID"] = value; }
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelValidator"/> class.
        /// </summary>
        public ModelValidator()
            : this(null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelValidator"/> class.
        /// </summary>
        /// <param name="controlValueResolver">The control value resolver.</param>
        /// <param name="validators">The validators.</param>
        /// <param name="validationScriptManager">The validation script manager.</param>
        /// <remarks>
        /// This constructor is for unit testing.
        /// </remarks>
        public ModelValidator(IControlValueResolver controlValueResolver)
        {
            _controlValueResolver = controlValueResolver ?? new ControlValueResolver(this);
        }

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
        /// </summary>
        /// <param name="e">A <see cref="T:System.EventArgs"/> that contains the event data.</param>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            Page.PreRenderComplete += Page_PreRenderComplete;
        }

        /// <summary>
        /// Handles the PreRenderComplete event of the Page control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void Page_PreRenderComplete(object sender, EventArgs e)
        {
            var type = GetModelType();
            object model = GetModelInstance(type);
            Dictionary<string, object> results;

            HttpContextWrapper contextWrapper = new HttpContextWrapper(this.Context);
            ControllerContext fakeContext = new ControllerContext
                                                {
                                                    HttpContext = contextWrapper
                                                };
            ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());

            foreach (ModelMetadata propertyMetadata in modelMetadata.Properties)
            {
                results = new Dictionary<string, object>();
                bool renderedRules = false;
                foreach (System.Web.Mvc.ModelValidator propertyValidator in propertyMetadata.GetValidators(fakeContext))
                {
                    foreach (var clientValidationRule in propertyValidator.GetClientValidationRules())
                    {

                        renderedRules = true;
                        string ruleName = "data-val-" + clientValidationRule.ValidationType;

                        ValidateUnobtrusiveValidationRule(clientValidationRule, results, ruleName);

                        results.Add(ruleName, HttpUtility.HtmlEncode(clientValidationRule.ErrorMessage ?? String.Empty));
                        ruleName += "-";

                        foreach (var kvp in clientValidationRule.ValidationParameters)
                        {
                            results.Add(ruleName + kvp.Key, kvp.Value ?? String.Empty);
                        }
                    }
                }


                //TODO Add Attribute to control
                if (renderedRules)
                {

                    results.Add("data-val", "true");
                    // Loop through all ModelProperties
                    String propertyName = propertyMetadata.PropertyName;

                    ModelProperty modelProperty =
                        ModelProperties.Where(x => x.PropertyName == propertyName).FirstOrDefault();
                    if (modelProperty != null)
                    {
                        // Try to find control.
                        WebControl webControl = (WebControl)Parent.FindControl(modelProperty.ControlToValidate);
                        if (webControl != null)
                        {
                            foreach (var result in results)
                            {
                                webControl.Attributes.Add(result.Key, result.Value.ToString());
                            }

                            var valMessageFor =
                                new LiteralControl(@"<span class='field-validation-valid' data-valmsg-for='" +
                                                   webControl.UniqueID + "' data-valmsg-replace='true'></span>");

                            webControl.Parent.Controls.AddAt(Parent.Controls.IndexOf(webControl) + 1, valMessageFor);

                        }
                        else
                        {
                            // No control was found so throw exception.
                            throw new InvalidOperationException(String.Format(
                                "Could not find contorol with ID: {0}",
                                modelProperty.ControlToValidate));
                        }

                    }

                }
            }
        }

        private static void ValidateUnobtrusiveValidationRule(ModelClientValidationRule rule, Dictionary<string, object> resultsDictionary, string dictionaryKey)
        {
            if (String.IsNullOrWhiteSpace(rule.ValidationType))
            {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        "MvcResources.HtmlHelper_ValidationTypeCannotBeEmpty",
                        rule.GetType().FullName
                    )
                );
            }

            if (resultsDictionary.ContainsKey(dictionaryKey))
            {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        "MvcResources.HtmlHelper_ValidationTypeMustBeUnique",
                        rule.ValidationType
                    )
                );
            }

            if (rule.ValidationType.Any(c => !Char.IsLower(c)))
            {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        "MvcResources.HtmlHelper_ValidationTypeMustBeLegal",
                        rule.ValidationType,
                        rule.GetType().FullName
                    )
                );
            }

            foreach (var key in rule.ValidationParameters.Keys)
            {
                if (String.IsNullOrWhiteSpace(key))
                {
                    throw new InvalidOperationException(
                        String.Format(
                            CultureInfo.CurrentCulture,
                           "MvcResources.HtmlHelper_ValidationParameterCannotBeEmpty",
                            rule.GetType().FullName
                        )
                    );
                }

                if (!Char.IsLower(key.First()) || key.Any(c => !Char.IsLower(c) && !Char.IsDigit(c)))
                {
                    throw new InvalidOperationException(
                        String.Format(
                            CultureInfo.CurrentCulture,
                            "MvcResources.HtmlHelper_ValidationParameterMustBeLegal",
                            key,
                            rule.GetType().FullName
                        )
                    );
                }
            }
        }

        /// <summary>
        /// Gets the type of the model.
        /// </summary>
        /// <returns></returns>
        private Type GetModelType()
        {
            if (_modelType == null)
            {
                if (String.IsNullOrEmpty(ModelType))
                {
                    throw new InvalidOperationException("ModelType must be set.");
                }

                _modelType = Type.GetType(ModelType);

                if (_modelType == null)
                {
                    // App_Code Types are created with System.Web.Compilation.BuildManager
                    _modelType = BuildManager.GetType(ModelType, false);
                }

                if (_modelType == null)
                {
                    throw new InvalidOperationException("Could not get a Type from " + ModelType);
                }
            }

            return _modelType;
        }

        /// <summary>
        /// Gets the model instance.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        private object GetModelInstance(Type type)
        {
            ConstructorInfo constructorInfo = type.GetConstructor(BindingFlags.Instance |
                                                                  BindingFlags.Public, null, new Type[0], null);

            if (constructorInfo == null)
            {
                // If no public constructor found try to get a non-public one.
                constructorInfo = type.GetConstructor(BindingFlags.Instance |
                                                      BindingFlags.NonPublic, null, new Type[0], null);

                // No default constructor found. Throw exception.
                if (constructorInfo == null)
                {
                    throw new InvalidOperationException("Could not find parameterless constructor for type: " +
                                                        ModelType);
                }
            }

            object model = constructorInfo.Invoke(null);

            if (model == null)
            {
                throw new InvalidOperationException("Could not create an instance of type: " + ModelType);
            }

            // update the model's properties with the values from the validated controls.
            foreach (ModelProperty modelProperty in ModelProperties)
            {
                PropertyInfo property = type.GetProperty(modelProperty.PropertyName);
                if (property == null)
                {
                    string message = String.Format("Could not find property {0} on type: {1}",
                                                   modelProperty.PropertyName, type);
                    throw new InvalidOperationException(message);
                }

                // get the value of the input control
                string valueString = _controlValueResolver.GetControlValue(modelProperty.ControlToValidate);
                object value;

                // get the underlying type of the nullable type (get int from int?).
                Type nullablePropertyType = Nullable.GetUnderlyingType(property.PropertyType);
                try
                {
                    if (nullablePropertyType != null)
                    {
                        value = Convert.ChangeType(valueString, nullablePropertyType);
                    }
                    else
                    {
                        if (property.PropertyType.IsEnum)
                        {
                            value = Enum.Parse(property.PropertyType, valueString);
                        }
                        else
                        {
                            value = Convert.ChangeType(valueString, property.PropertyType);
                        }
                    }
                }
                catch (FormatException)
                {
                    value = null;
                }

                if (value != null)
                {
                    // set the property value
                    property.SetValue(model, value, null);
                }
            }

            return model;
        }

    }
}
